package com.linkoog.devtools.archive.util;

import com.linkoog.devtools.archive.filesystem.ArchiveHandlerBase;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.sevenz.SevenZFile;
import org.apache.commons.compress.utils.IOUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

public class ArchiveUtils {

    private static final Logger log = Logger.getInstance(ArchiveUtils.class);

    public static final long DEFAULT_LENGTH = 0L;
    public static final long DEFAULT_TIMESTAMP = -1L;

    public static byte[] getEntryBytes(ArchiveInputStream inputStream, ArchiveEntry item) throws IOException {
        ArchiveEntry entry;
        while ((entry = inputStream.getNextEntry()) != null) {
            if (!Objects.equals(item.getName(), entry.getName())) {
                continue;
            }

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            IOUtils.copy(inputStream, outputStream);
            return outputStream.toByteArray();
        }

        return new byte[0];
    }

    public static byte[] getEntryBytes(SevenZFile file, ArchiveEntry item) throws IOException {
        ArchiveEntry entry;
        while ((entry = file.getNextEntry()) != null) {
            if (!Objects.equals(item.getName(), entry.getName())) {
                continue;
            }

            //TODO: fixme
            byte[] bytes = new byte[(int) entry.getSize()];
            //byte[] bytes = new byte[entry.getSize()];
            file.read(bytes);
            return bytes;
        }

        return new byte[0];
    }

    /**
     * @param entries this archive's entries name map
     *
     * @return constructed a archive entries info map
     */
    @NotNull
    public static Map<String, ArchiveHandlerBase.EntryInfo> buildEntryMap(Map<String, ArchiveEntry> entries) {
        Map<String, ArchiveHandlerBase.EntryInfo> map = new HashMap<>(entries.size());
        map.put("", createRootEntry());

        for (Map.Entry<String, ArchiveEntry> entry : entries.entrySet()) {
            getOrCreate(entry.getValue(), map, entries);
        }
        return map;
    }

    /**
     * wrap the given {@link ArchiveEntry} to a {@link ArchiveHandlerBase.EntryInfo}
     *
     * @param map map result
     * @param entries all entries in this archive
     * @return
     */
    public static ArchiveHandlerBase.EntryInfo getOrCreate(@NotNull ArchiveEntry entry, @NotNull Map<String, ArchiveHandlerBase.EntryInfo> map, @NotNull Map<String, ArchiveEntry> entries) {
        boolean isDirectory = entry.isDirectory();
        String entryName = entry.getName();
        if (StringUtil.endsWithChar(entryName, '/')) {
            entryName = entryName.substring(0, entryName.length() - 1);
            isDirectory = true;
        }

        //get from cache
        ArchiveHandlerBase.EntryInfo info = map.get(entryName);
        if (info != null) {
            return info;
        }

        //create node
        Pair<String, String> path = splitPath(entryName);
        ArchiveHandlerBase.EntryInfo parentInfo = getOrCreate(path.first, map, entries);
        if (".".equals(path.second)) {
            return parentInfo;
        }
        info = store(map, parentInfo, path.second, isDirectory, entry.getSize(), entry.getLastModifiedDate().getTime(), entryName);
        return info;
    }

    @NotNull
    public static ArchiveHandlerBase.EntryInfo getOrCreate(@NotNull String entryName, @NotNull Map<String, ArchiveHandlerBase.EntryInfo> map, @NotNull Map<String, ArchiveEntry> entries) {
        ArchiveHandlerBase.EntryInfo info = map.get(entryName);

        if (info == null) {
            ArchiveEntry entry = entries.get(entryName + '/');
            if (entry != null) {
                return getOrCreate(entry, map, entries);
            }

            Pair<String, String> path = splitPath(entryName);

            ArchiveHandlerBase.EntryInfo parentInfo = getOrCreate(path.first, map, entries);
            info = store(map, parentInfo, path.second, true, DEFAULT_LENGTH, DEFAULT_TIMESTAMP, entryName);
        }

        if (!info.isDirectory) {
            log.info(entryName + " should be a directory");
            info = store(map, (ArchiveHandlerBase.EntryInfo) info.parent, info.shortName.toString(), true, info.length, info.timestamp, entryName);
        }

        return info;
    }

    @NotNull
    public static Map<String, ArchiveEntry> calculateEntries(ArchiveInputStream input) throws IOException {
        Map<String, ArchiveEntry> map = new LinkedHashMap<>();
        ArchiveEntry entry;
        while ((entry = input.getNextEntry()) != null) {
            map.put(entry.getName(), entry);
        }
        return map;
    }

    @NotNull
    public static Map<String, ArchiveEntry> calculateEntries(SevenZFile file) {
        Map<String, ArchiveEntry> map = new LinkedHashMap<>();
        file.getEntries().forEach(sevenZArchiveEntry -> map.put(sevenZArchiveEntry.getName(), sevenZArchiveEntry));
        return map;
    }

    @NotNull
    public static ArchiveHandlerBase.EntryInfo createRootEntry() {
        return new ArchiveHandlerBase.EntryInfo("", true, DEFAULT_LENGTH, DEFAULT_TIMESTAMP, null);
    }

    @NotNull
    private static ArchiveHandlerBase.EntryInfo store(@NotNull Map<String, ArchiveHandlerBase.EntryInfo> map,
                                                      @Nullable ArchiveHandlerBase.EntryInfo parentInfo,
                                                      @NotNull String shortName,
                                                      boolean isDirectory,
                                                      long size,
                                                      long time,
                                                      @NotNull String entryName) {
        ArchiveHandlerBase.EntryInfo info = new ArchiveHandlerBase.EntryInfo(shortName, isDirectory, size, time, parentInfo);
        map.put(entryName, info);
        return info;
    }

    @NotNull
    private static Pair<String, String> splitPath(@NotNull String entryName) {
        int p = entryName.lastIndexOf('/');
        String parentName = p > 0 ? entryName.substring(0, p) : "";
        String shortName = p > 0 ? entryName.substring(p + 1) : entryName;
        return Pair.create(parentName, shortName);
    }

}